Stateless WebSockets with Express and Pushpin
One of the most interesting features of the Pushpin proxy is its ability to gateway between WebSocket clients and plain HTTP backend servers. In this article, we’ll demonstrate how to build a WebSocket service using Express as the HTTP backend behind Pushpin.
Wait, HTTP?
Both Pushpin and Node support WebSockets natively, so you might wonder why you’d want to use Pushpin’s HTTP-gatewaying feature between the two. The reason is that it enables stateless backend development. Often, WebSocket services send messages to clients in two cases:
- In reaction to an incoming message from the client (e.g. some kind of RPC).
- To relay a spontaneous “push” event generated by a backend service.
When building a WebSocket service using Pushpin, you’d almost always want to handle the second case by publishing messages to Pushpin out-of-band. In theory, this means that the backend server should only need to send messages over the proxied connection to the client if it’s responding to a message that the client had just sent. This may not be the case with all WebSocket services, but it likely applies to many of them, if not most.
The resulting architecture looks like this:
The Express application only needs to be able to handle short-lived HTTP requests, and make outbound HTTP POST requests whenever there is data to “push”. The Express application does not need to maintain any long-lived, stateful connections.
By trading WebSockets for HTTP at the proxy->backend path, we gain some nice things:
- Simpler code. No stateful connections in the backend makes it easier to understand, maintain, and test.
- Load balancing requests associated with a single client connection to a set of backends.
- Hot reload. Restarting the backend doesn’t disconnect clients.
- Less sockets between proxy and backend. If you have a multi-tiered architecture and a million client connections, it’s preferable to avoid having a million sockets at each tier.
- Scaling to handle more listening connections is straightforward: just add more Pushpin instances.
WebSocket-over-HTTP protocol
When Pushpin gateways a WebSocket connection over HTTP, events from the WebSocket connection are encoded into HTTP requests and sent to the backend server. If the server wishes to send events back to the WebSocket client, it encodes events in the HTTP responses.
Below are some example exchanges. The format is inspired by HTTP chunked encoding. Note that the characters \r\n
represent a two-byte carriage return and newline sequence. Linebreaks are also inserted for readability.
Proxy tells the backend server about a new connection:
Backend server accepts connection:
Proxy relays message from client:
Backend server responds with two messages:
For details, see the spec. Note that it’s not necessary to understand the protocol since the express-grip library (discussed below) takes care of it for you.
express-grip and mock socket object
The express-grip library provides a socket-like object called WebSocketContext
that handles the event marshalling over HTTP. The object contains methods like accept()
, send()
, recv()
, etc. What’s interesting is that these methods don’t operate directly on a real WebSocket. When recv()
is called, it simply iterates over the events received in the current HTTP request. When send()
is called, events are temporarily enqueued and a middleware serializes them at the end into the HTTP response. WebSocketContext
objects are not long-lived, and a fresh one is created for each handler invocation and destroyed afterwards.
Code
We’ll make a simple echo & broadcast service. There will be two endpoints:
/websocket
- Endpoint for WebSocket clients to connect to. Any received messages will be echoed back to the client./broadcast
- HTTP endpoint. AcceptsPOST
requests, and the request body is sent as a string to all connected WebSocket clients.
Pushpin will be configured with the route:
This means to route all incoming traffic to port 3000 on the local machine, with WebSocket connections gatewayed as HTTP.
Below is the full code for the Express backend. Afterwards, we’ll go over the important parts of it.
The first thing you’ll notice is Pushpin configuration:
This tells the express-grip library where published data should be sent to. We’re using Pushpin’s default control port on the local machine.
We also set up some middleware. There are two middleware handlers. One that goes before everything and one that goes after:
With that boilerplate out of the way, let’s look at the actual handlers. In the /websocket
handler, we accept all incoming connections and immediately subscribe them to a channel called all
:
If the HTTP request contained a WebSocket OPEN
event, then isOpening()
will return true. Calling accept()
simply sets an internal flag. When the handler finishes, the postHandlerGripMiddleware
will see that this flag was set and include an OPEN
event in the HTTP response that it sends back to the proxy. The subscribe()
call is a shortcut for sending a GRIP subscribe control message as a TEXT
event.
Then we process any incoming messages in a loop. If the HTTP request contained a CLOSE
event, then recv()
will return null
. Similar to accept()
, calling close()
sets an internal flag telling the middleware to include a CLOSE
event on the way out.
Lastly, in the /broadcast
handler we publish a WebSocket message to the all
channel, which every connection is subscribed to:
As with the /websocket
handler, this handler is also stateless. The publish()
call is fire-and-forget.
That’s all there is to it! The backend code looks like typical WebSocket handling logic, except it’s completely faked and stateless.
Recent posts
-
We've been acquired by Fastly
-
A cloud-native platform for push APIs
-
Vercel and WebSockets
-
Rewriting Pushpin's connection manager in Rust
-
Let's Encrypt for custom domains